import time
import logging
import json
import smtplib
import os
from datetime import datetime
from typing import Dict
import paho.mqtt.client as mqtt
import threading
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

# ==============================================================================
# 配置参数
# ==============================================================================

# MQTT 服务器配置
MQTT_CONFIG = {
  "broker": "114.215.197.224",
  "port": 1883,
  "keepalive": 60,
  "username": "intranet",
  "password": "1qaz@WSX"
}

# 业务逻辑配置
BUSINESS_CONFIG = {
  "base_topic": "IOT/intranet/server/report/test",
}

# 邮件警报配置
EMAIL_CONFIG = {
  "smtp_server": "smtp.qq.com",
  "smtp_port": 465,
  "sender_email": "1448596331@qq.com",
  "sender_password": "sqnjnsqkmyuufhee",
  "recipient_email": "1448596331@qq.com",
}

# 要监测的设备ID列表
DEVICE_IDS = ["SC2212244240046", "869298057410201"]

# 设备类型配置
DEVICE_CONFIGS = {
  "7621": {
    "identifier_keys": ["v12", "v24", "v5", "volumeAdaptSchema"],
    "imei_prefixes": ["SC", "2K", "2020"]
  },
  "HZ": {
    "identifier_keys": ["relayList", "relayMode"],
    "imei_prefixes": ["86"]
  }
}

# 离线检测倍数配置
OFFLINE_MULTIPLIER = 2

# 日志文件配置
LOG_CONFIG = {
  "log_file": "mqtt_heartbeat_monitor.log",
  "max_size_mb": 50,  # 最大50MB
  "backup_count": 1  # 保留5个备份文件
}


# ==============================================================================
# 日志配置
# ==============================================================================

class UnicodeSafeStreamHandler(logging.StreamHandler):
  def emit(self, record):
    try:
      msg = self.format(record)
      stream = self.stream
      try:
        stream.write(msg + self.terminator)
      except UnicodeEncodeError:
        safe_msg = msg.encode('utf-8', errors='ignore').decode('utf-8')
        stream.write(safe_msg + self.terminator)
      self.flush()
    except Exception:
      self.handleError(record)


def setup_logging():
  """设置日志配置，包括文件大小检查"""
  # 检查并清理日志文件
  check_and_clean_log_file()

  # 配置日志
  logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[
      logging.FileHandler(LOG_CONFIG["log_file"], encoding='utf-8'),
      UnicodeSafeStreamHandler()
    ]
  )
  return logging.getLogger(__name__)


def check_and_clean_log_file():
  """检查日志文件大小，如果超过限制则清理"""
  log_file = LOG_CONFIG["log_file"]

  if not os.path.exists(log_file):
    return

  # 获取文件大小（MB）
  file_size_mb = os.path.getsize(log_file) / (1024 * 1024)

  if file_size_mb > LOG_CONFIG["max_size_mb"]:
    try:
      # 创建备份文件
      backup_files = []
      for i in range(LOG_CONFIG["backup_count"] - 1, 0, -1):
        old_file = f"{log_file}.{i}"
        new_file = f"{log_file}.{i + 1}"
        if os.path.exists(old_file):
          if os.path.exists(new_file):
            os.remove(new_file)
          os.rename(old_file, new_file)
          backup_files.append(new_file)

      # 重命名当前日志文件为 .1
      if os.path.exists(f"{log_file}.1"):
        os.remove(f"{log_file}.1")
      os.rename(log_file, f"{log_file}.1")
      backup_files.append(f"{log_file}.1")

      # 删除多余的备份文件
      for i in range(LOG_CONFIG["backup_count"] + 1, 10):
        extra_file = f"{log_file}.{i}"
        if os.path.exists(extra_file):
          os.remove(extra_file)

      logger = logging.getLogger(__name__)
      logger.info(f"日志文件已清理，原大小: {file_size_mb:.2f}MB，创建备份: {backup_files}")

    except Exception as e:
      # 如果清理失败，使用基本日志记录错误
      temp_logger = logging.getLogger(__name__)
      if not temp_logger.handlers:
        temp_logger.addHandler(logging.StreamHandler())
      temp_logger.error(f"清理日志文件失败: {e}")


def rotate_logs_periodically():
  """定期检查日志文件大小"""
  while True:
    try:
      check_and_clean_log_file()
      # 每1小时检查一次
      time.sleep(3600)
    except Exception as e:
      logger.error(f"日志轮询检查出错: {e}")
      time.sleep(3600)


# 初始化日志
logger = setup_logging()


# ==============================================================================
# MQTT 心跳监测类
# ==============================================================================

class MQTTHeartbeatMonitor:
  def __init__(self):
    self.last_heartbeat: Dict[str, float] = {}
    self.device_types: Dict[str, str] = {}
    self.device_intervals: Dict[str, int] = {}
    self.device_topics = {}
    self.offline_alerts_sent: Dict[str, bool] = {}
    self.monitoring = True

    # 生成设备topic映射
    base_topic = BUSINESS_CONFIG["base_topic"].rstrip('/')
    for device_id in DEVICE_IDS:
      self.device_topics[device_id] = f"{base_topic}/{device_id}"
      self.offline_alerts_sent[device_id] = False
      self.device_intervals[device_id] = 60

    logger.info(f"监控设备: {DEVICE_IDS}")

    # MQTT客户端初始化
    self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    if MQTT_CONFIG["username"] and MQTT_CONFIG["password"]:
      self.client.username_pw_set(MQTT_CONFIG["username"], MQTT_CONFIG["password"])

    self.client.on_connect = self.on_connect
    self.client.on_message = self.on_message
    self.client.on_subscribe = self.on_subscribe
    self.client.on_disconnect = self.on_disconnect

    # 启动离线检测线程
    self.offline_check_thread = threading.Thread(target=self.check_offline_devices, daemon=True)
    self.offline_check_thread.start()

    # 启动日志轮询线程
    self.log_rotate_thread = threading.Thread(target=rotate_logs_periodically, daemon=True)
    self.log_rotate_thread.start()

  def send_email_alert(self, subject, body):
    """发送电子邮件警报"""
    try:
      msg = MIMEMultipart()
      msg['From'] = EMAIL_CONFIG["sender_email"]
      msg['To'] = EMAIL_CONFIG["recipient_email"]
      msg['Subject'] = subject

      msg.attach(MIMEText(body, 'plain', 'utf-8'))

      with smtplib.SMTP_SSL(EMAIL_CONFIG["smtp_server"], EMAIL_CONFIG["smtp_port"]) as server:
        server.login(EMAIL_CONFIG["sender_email"], EMAIL_CONFIG["sender_password"])
        server.sendmail(EMAIL_CONFIG["sender_email"], EMAIL_CONFIG["recipient_email"], msg.as_string())

      logger.info(f"警报邮件已发送: {subject}")
    except Exception as e:
      logger.error(f"发送警报邮件失败: {e}")

  def check_offline_devices(self):
    """定期检查设备是否离线"""
    while self.monitoring:
      try:
        current_time = time.time()
        for device_id in self.device_topics.keys():
          if device_id in self.last_heartbeat:
            interval_time = self.device_intervals.get(device_id, 60)
            offline_timeout = interval_time * OFFLINE_MULTIPLIER

            time_since_last_heartbeat = current_time - self.last_heartbeat[device_id]

            if time_since_last_heartbeat > offline_timeout:
              if not self.offline_alerts_sent[device_id]:
                device_type = self.device_types.get(device_id, "未知")
                last_time_str = datetime.fromtimestamp(
                  self.last_heartbeat[device_id]
                ).strftime("%Y-%m-%d %H:%M:%S")

                offline_seconds = int(time_since_last_heartbeat)
                alert_subject = f"设备离线警报 - {device_id}"
                alert_body = (
                  f"设备ID：{device_id}\n"
                  f"设备类型：{device_type}\n"
                  f"已离线：{offline_seconds} 秒\n"
                  f"最后心跳时间：{last_time_str}\n"
                  f"设备上报间隔：{interval_time} 秒\n"
                  f"离线判定阈值：{offline_timeout} 秒 (2倍间隔)"
                )
                logger.error(f"设备[{device_id}]已离线 {offline_seconds}秒")
                self.send_email_alert(alert_subject, alert_body)
                self.offline_alerts_sent[device_id] = True
            else:
              self.offline_alerts_sent[device_id] = False

        time.sleep(30)
      except Exception as e:
        logger.error(f"离线检查线程出错: {e}")
        time.sleep(60)

  def detect_device_type(self, payload: dict) -> str:
    """根据消息内容自动识别设备类型"""
    for device_type, config in DEVICE_CONFIGS.items():
      for key in config["identifier_keys"]:
        if key in payload:
          return device_type

    imei = payload.get("imei", "")
    if imei:
      for device_type, config in DEVICE_CONFIGS.items():
        for prefix in config["imei_prefixes"]:
          if imei.startswith(prefix):
            return device_type

    logger.warning(f"无法识别设备类型，IMEI: {imei}")
    return "HZ"

  def on_connect(self, client, userdata, flags, reason_code, properties):
    if reason_code == 0:
      logger.info("MQTT连接成功")
      for device_id, topic in self.device_topics.items():
        client.subscribe(topic, qos=1)
        logger.info(f"已订阅设备[{device_id}] -> {topic}")
    else:
      logger.error(f"MQTT连接失败，错误码：{reason_code}")

  def on_subscribe(self, client, userdata, mid, granted_qos, properties):
    logger.debug(f"订阅确认: MID={mid}, QoS={granted_qos}")

  def on_disconnect(self, client, userdata, reason_code, properties):
    logger.warning(f"MQTT断开连接，原因: {reason_code}")

  def on_message(self, client, userdata, msg):
    try:
      topic = msg.topic
      payload_str = msg.payload.decode('utf-8', errors='ignore').strip()

      device_id = next((d for d, t in self.device_topics.items() if t == topic), None)
      if not device_id:
        return

      try:
        payload = json.loads(payload_str)
        cmd = payload.get("cmd", "")
        if cmd != "hello":
          return
      except json.JSONDecodeError:
        return

      # 更新设备的心跳间隔时间
      interval_time = payload.get("intervalTime")
      if interval_time and isinstance(interval_time, (int, float)):
        self.device_intervals[device_id] = interval_time
        logger.debug(f"设备[{device_id}]心跳间隔: {interval_time}秒")

      # 识别设备类型
      if device_id not in self.device_types:
        self.device_types[device_id] = self.detect_device_type(payload)
        logger.info(f"识别设备[{device_id}]类型为: {self.device_types[device_id]}")

      device_type = self.device_types[device_id]
      current_time = time.time()
      time_str = datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S")

      # 处理心跳消息
      is_first_heartbeat = device_id not in self.last_heartbeat
      last_time = self.last_heartbeat.get(device_id, current_time)
      interval = round(current_time - last_time, 2)
      self.last_heartbeat[device_id] = current_time
      self.offline_alerts_sent[device_id] = False

      if is_first_heartbeat:
        logger.info(f"设备[{device_id}]({device_type})首次心跳 | 时间：{time_str}")
        return

      # 动态计算异常阈值
      threshold = self.device_intervals[device_id] * 1.5
      if interval <= threshold:
        logger.info(f"设备[{device_id}]({device_type})心跳正常 | 间隔：{interval}秒")
      else:
        alert_subject = f"设备心跳异常 - {device_id}"
        alert_body = (
          f"设备ID：{device_id}\n"
          f"设备类型：{device_type}\n"
          f"异常间隔：{interval}秒\n"
          f"设备上报间隔：{self.device_intervals[device_id]}秒\n"
          f"异常阈值：{threshold}秒\n"
          f"发生时间：{time_str}"
        )
        logger.error(f"设备[{device_id}]心跳异常 | 间隔：{interval}秒")
        self.send_email_alert(alert_subject, alert_body)

    except Exception as e:
      logger.error(f"处理消息时出错: {e}")

  def start(self):
    logger.info("=" * 50)
    logger.info("MQTT心跳监测工具启动")
    logger.info(f"警报邮箱: {EMAIL_CONFIG['recipient_email']}")
    logger.info(f"监测设备: {len(DEVICE_IDS)}台")
    logger.info(f"日志限制: {LOG_CONFIG['max_size_mb']}MB，保留{LOG_CONFIG['backup_count']}个备份")
    logger.info("=" * 50)

    try:
      self.client.connect(MQTT_CONFIG["broker"], MQTT_CONFIG["port"], MQTT_CONFIG["keepalive"])
      self.client.loop_forever()
    except KeyboardInterrupt:
      logger.info("用户手动停止监测")
      self.monitoring = False
      self.client.disconnect()
    except Exception as e:
      logger.critical(f"MQTT连接失败：{e}")
      self.monitoring = False


if __name__ == "__main__":
  try:
    monitor = MQTTHeartbeatMonitor()
    monitor.start()
  except Exception as e:
    logger.critical(f"监测工具启动失败：{e}")